<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Mail
 * @subpackage Protocol
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Pop3.php 18731 2009-10-29 04:18:19Z yoshida@zend.co.jp $
 */


/**
 * @category   Zend
 * @package    Zend_Mail
 * @subpackage Protocol
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Mail_Protocol_Pop3
{
	/**
	 * Default timeout in seconds for initiating session
	 */
	const TIMEOUT_CONNECTION = 30;

	/**
	 * saves if server supports top
	 * @var null|bool
	 */
	public $hasTop = null;

	/**
	 * socket to pop3
	 * @var null|resource
	 */
	protected $_socket;

	/**
	 * greeting timestamp for apop
	 * @var null|string
	 */
	protected $_timestamp;


	/**
	 * Public constructor
	 *
	 * @param  string      $host  hostname or IP address of POP3 server, if given connect() is called
	 * @param  int|null    $port  port of POP3 server, null for default (110 or 995 for ssl)
	 * @param  bool|string $ssl   use ssl? 'SSL', 'TLS' or false
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function __construct($host = '', $port = null, $ssl = false)
	{
		if ($host) {
			$this->connect($host, $port, $ssl);
		}
	}


	/**
	 * Public destructor
	 */
	public function __destruct()
	{
		$this->logout();
	}


	/**
	 * Open connection to POP3 server
	 *
	 * @param  string      $host  hostname or IP address of POP3 server
	 * @param  int|null    $port  of POP3 server, default is 110 (995 for ssl)
	 * @param  string|bool $ssl   use 'SSL', 'TLS' or false
	 * @return string welcome message
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function connect($host, $port = null, $ssl = false)
	{
		if ($ssl == 'SSL') {
			$host = 'ssl://' . $host;
		}

		if ($port === null) {
			$port = $ssl == 'SSL' ? 995 : 110;
		}

		$errno  =  0;
		$errstr = '';
		$this->_socket = @fsockopen($host, $port, $errno, $errstr, self::TIMEOUT_CONNECTION);
		if (!$this->_socket) {
			/**
			 * @see Zend_Mail_Protocol_Exception
			 */
			require_once 'Zend/Mail/Protocol/Exception.php';
			throw new Zend_Mail_Protocol_Exception('cannot connect to host; error = ' . $errstr .
                                                   ' (errno = ' . $errno . ' )');
		}

		$welcome = $this->readResponse();

		strtok($welcome, '<');
		$this->_timestamp = strtok('>');
		if (!strpos($this->_timestamp, '@')) {
			$this->_timestamp = null;
		} else {
			$this->_timestamp = '<' . $this->_timestamp . '>';
		}

		if ($ssl === 'TLS') {
			$this->request('STLS');
			$result = stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
			if (!$result) {
				/**
				 * @see Zend_Mail_Protocol_Exception
				 */
				require_once 'Zend/Mail/Protocol/Exception.php';
				throw new Zend_Mail_Protocol_Exception('cannot enable TLS');
			}
		}

		return $welcome;
	}


	/**
	 * Send a request
	 *
	 * @param string $request your request without newline
	 * @return null
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function sendRequest($request)
	{
		$result = @fputs($this->_socket, $request . "\r\n");
		if (!$result) {
			/**
			 * @see Zend_Mail_Protocol_Exception
			 */
			require_once 'Zend/Mail/Protocol/Exception.php';
			throw new Zend_Mail_Protocol_Exception('send failed - connection closed?');
		}
	}


	/**
	 * read a response
	 *
	 * @param  boolean $multiline response has multiple lines and should be read until "<nl>.<nl>"
	 * @return string response
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function readResponse($multiline = false)
	{
		$result = @fgets($this->_socket);
		if (!is_string($result)) {
			/**
			 * @see Zend_Mail_Protocol_Exception
			 */
			require_once 'Zend/Mail/Protocol/Exception.php';
			throw new Zend_Mail_Protocol_Exception('read failed - connection closed?');
		}

		$result = trim($result);
		if (strpos($result, ' ')) {
			list($status, $message) = explode(' ', $result, 2);
		} else {
			$status = $result;
			$message = '';
		}

		if ($status != '+OK') {
			/**
			 * @see Zend_Mail_Protocol_Exception
			 */
			require_once 'Zend/Mail/Protocol/Exception.php';
			throw new Zend_Mail_Protocol_Exception('last request failed');
		}

		if ($multiline) {
			$message = '';
			$line = fgets($this->_socket);
			while ($line && rtrim($line, "\r\n") != '.') {
				if ($line[0] == '.') {
					$line = substr($line, 1);
				}
				$message .= $line;
				$line = fgets($this->_socket);
			};
		}

		return $message;
	}


	/**
	 * Send request and get resposne
	 *
	 * @see sendRequest(), readResponse()
	 *
	 * @param  string $request    request
	 * @param  bool   $multiline  multiline response?
	 * @return string             result from readResponse()
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function request($request, $multiline = false)
	{
		$this->sendRequest($request);
		return $this->readResponse($multiline);
	}


	/**
	 * End communication with POP3 server (also closes socket)
	 *
	 * @return null
	 */
	public function logout()
	{
		if (!$this->_socket) {
			return;
		}

		try {
			$this->request('QUIT');
		} catch (Zend_Mail_Protocol_Exception $e) {
			// ignore error - we're closing the socket anyway
		}

		fclose($this->_socket);
		$this->_socket = null;
	}


	/**
	 * Get capabilities from POP3 server
	 *
	 * @return array list of capabilities
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function capa()
	{
		$result = $this->request('CAPA', true);
		return explode("\n", $result);
	}


	/**
	 * Login to POP3 server. Can use APOP
	 *
	 * @param  string $user      username
	 * @param  string $password  password
	 * @param  bool   $try_apop  should APOP be tried?
	 * @return void
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function login($user, $password, $tryApop = true)
	{
		if ($tryApop && $this->_timestamp) {
			try {
				$this->request("APOP $user " . md5($this->_timestamp . $password));
				return;
			} catch (Zend_Mail_Protocol_Exception $e) {
				// ignore
			}
		}

		$result = $this->request("USER $user");
		$result = $this->request("PASS $password");
	}


	/**
	 * Make STAT call for message count and size sum
	 *
	 * @param  int $messages  out parameter with count of messages
	 * @param  int $octets    out parameter with size in octects of messages
	 * @return void
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function status(&$messages, &$octets)
	{
		$messages = 0;
		$octets = 0;
		$result = $this->request('STAT');

		list($messages, $octets) = explode(' ', $result);
	}


	/**
	 * Make LIST call for size of message(s)
	 *
	 * @param  int|null $msgno number of message, null for all
	 * @return int|array size of given message or list with array(num => size)
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function getList($msgno = null)
	{
		if ($msgno !== null) {
			$result = $this->request("LIST $msgno");

			list(, $result) = explode(' ', $result);
			return (int)$result;
		}

		$result = $this->request('LIST', true);
		$messages = array();
		$line = strtok($result, "\n");
		while ($line) {
			list($no, $size) = explode(' ', trim($line));
			$messages[(int)$no] = (int)$size;
			$line = strtok("\n");
		}

		return $messages;
	}


	/**
	 * Make UIDL call for getting a uniqueid
	 *
	 * @param  int|null $msgno number of message, null for all
	 * @return string|array uniqueid of message or list with array(num => uniqueid)
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function uniqueid($msgno = null)
	{
		if ($msgno !== null) {
			$result = $this->request("UIDL $msgno");

			list(, $result) = explode(' ', $result);
			return $result;
		}

		$result = $this->request('UIDL', true);

		$result = explode("\n", $result);
		$messages = array();
		foreach ($result as $line) {
			if (!$line) {
				continue;
			}
			list($no, $id) = explode(' ', trim($line), 2);
			$messages[(int)$no] = $id;
		}

		return $messages;

	}


	/**
	 * Make TOP call for getting headers and maybe some body lines
	 * This method also sets hasTop - before it it's not known if top is supported
	 *
	 * The fallback makes normale RETR call, which retrieves the whole message. Additional
	 * lines are not removed.
	 *
	 * @param  int  $msgno    number of message
	 * @param  int  $lines    number of wanted body lines (empty line is inserted after header lines)
	 * @param  bool $fallback fallback with full retrieve if top is not supported
	 * @return string message headers with wanted body lines
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function top($msgno, $lines = 0, $fallback = false)
	{
		if ($this->hasTop === false) {
			if ($fallback) {
				return $this->retrieve($msgno);
			} else {
				/**
				 * @see Zend_Mail_Protocol_Exception
				 */
				require_once 'Zend/Mail/Protocol/Exception.php';
				throw new Zend_Mail_Protocol_Exception('top not supported and no fallback wanted');
			}
		}
		$this->hasTop = true;

		$lines = (!$lines || $lines < 1) ? 0 : (int)$lines;

		try {
			$result = $this->request("TOP $msgno $lines", true);
		} catch (Zend_Mail_Protocol_Exception $e) {
			$this->hasTop = false;
			if ($fallback) {
				$result = $this->retrieve($msgno);
			} else {
				throw $e;
			}
		}

		return $result;
	}


	/**
	 * Make a RETR call for retrieving a full message with headers and body
	 *
	 * @deprecated since 1.1.0; this method has a typo - please use retrieve()
	 * @param  int $msgno  message number
	 * @return string message
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function retrive($msgno)
	{
		return $this->retrieve($msgno);
	}


	/**
	 * Make a RETR call for retrieving a full message with headers and body
	 *
	 * @param  int $msgno  message number
	 * @return string message
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function retrieve($msgno)
	{
		$result = $this->request("RETR $msgno", true);
		return $result;
	}

	/**
	 * Make a NOOP call, maybe needed for keeping the server happy
	 *
	 * @return null
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function noop()
	{
		$this->request('NOOP');
	}


	/**
	 * Make a DELE count to remove a message
	 *
	 * @return null
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function delete($msgno)
	{
		$this->request("DELE $msgno");
	}


	/**
	 * Make RSET call, which rollbacks delete requests
	 *
	 * @return null
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function undelete()
	{
		$this->request('RSET');
	}
}
